/// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © RafaelZioni — MTF candle volume profile built from lower-timeframe candles — v5 (v3 display + close predictor)
//
// Three timeframes:
//   Profile TF : the candle you want the profile of (5m, 15m, 1h, 4h, D...). Empty = chart TF.
//   Chart TF   : whatever the chart is on. Must be <= Profile TF and divide it evenly.
//   Source TF  : the candles the volume is built from (1m, 15S...). Must be <= Chart TF.
//
// Display (as v3):
//   Chart TF <  Profile TF -> full horizontal profile drawn across the chart bars of each profile candle
//   Chart TF == Profile TF -> heatmap inside each candle + POC dot + VA ticks, live profile right of last bar
//   "Pick a candle"        -> profile of any chosen profile-TF candle, drawn right of the last bar
//
// Close predictor (new):
//   Live P(profile candle closes >= its open) from three forces measured so far inside the candle:
//     z  = (price - open) / (sigma * sqrt(time left))   -> how hard it is to undo the move in the time left
//     dp = delta: (up vol - down vol) / total, -1..1     -> who is pushing
//     pb = (POC - open) / sigma                          -> where volume has accepted price vs the open
//   P(up) = Phi(z + wD*dp + wP*pb)
//   Calibration table scores every finished candle in history at a fixed checkpoint (e.g. 2 of 5 minutes),
//   so you can see which force actually predicts and tune wD / wP.

//@version=6
indicator("Candle Volume Profile MTF + Close Predictor", "CVP-MTF", overlay=true, max_boxes_count=500, max_lines_count=500, max_labels_count=100, max_bars_back=5000)

// ───────── Inputs
profTF   = input.timeframe("", "Profile timeframe (empty = chart)")
srcTF    = input.timeframe("1", "Source timeframe (volume from)")
rows     = input.int(24, "Rows", minval=4, maxval=60)
vaPct    = input.float(70, "Value area %", minval=10, maxval=100) / 100
cBuy     = input.color(color.teal, "Buy colour")
cSell    = input.color(color.red,  "Sell colour")
cPoc     = input.color(color.yellow, "POC colour")
cVA      = input.color(color.gray, "VAH/VAL colour")
fadeOut  = input.int(50, "Extra transparency outside VA", minval=0, maxval=100)

gP       = "Profile across bars (chart TF < profile TF)"
showN    = input.int(8, "Draw last N profile candles", minval=1, maxval=100, group=gP, tooltip="Boxes are capped at 500: rows x 2 x N")
widthPct = input.float(90, "Max width % of the profile candle", minval=10, maxval=100, group=gP)
showVAL  = input.bool(true, "VAH / VAL lines", group=gP)
showDlt  = input.bool(true, "Delta % label", group=gP)
showBox  = input.bool(true, "Outline profile candle range", group=gP)

gH       = "Inside each candle (chart TF = profile TF)"
showHM   = input.bool(true, "Heatmap rows", group=gH)
hmRows   = input.int(8, "Heatmap rows", minval=2, maxval=8, group=gH)
hmMinT   = input.int(25, "Transparency – heaviest row", minval=0, maxval=100, group=gH)
hmMaxT   = input.int(90, "Transparency – lightest row", minval=0, maxval=100, group=gH)
showPOC  = input.bool(true, "POC dot", group=gH)
showVAk  = input.bool(true, "VAH / VAL ticks", group=gH)

gL       = "Right of last bar"
lvWidth  = input.int(15, "Width (bars)", minval=3, maxval=80, group=gL)
lvOff    = input.int(3, "Offset from last bar", minval=1, maxval=50, group=gL)
showLv   = input.bool(true, "Live + previous candle (only when chart TF = profile TF)", group=gL)
selOn    = input.bool(false, "Pick a candle", group=gL)
selT     = input.time(timestamp("2026-10-19T00:00:00"), "Candle time (drag on chart)", group=gL)

gF       = "Close predictor"
predOn   = input.bool(true, "Show predictor", group=gF)
chkM     = input.int(2, "Calibration checkpoint (source candles elapsed)", minval=1, maxval=500, group=gF, tooltip="5m candle from 1m: 2 = after 2 minutes (40%). Must be less than candles per profile candle.")
wD       = input.float(0.5, "Weight: delta", step=0.1, group=gF)
wP       = input.float(0.5, "Weight: POC vs open", step=0.1, group=gF)
sigLen   = input.int(100, "Sigma lookback (profile candles)", minval=20, maxval=1000, group=gF)
tblPos   = input.string("top_right", "Table position", options=["top_right", "bottom_right", "top_left", "bottom_left"], group=gF)
tblSzS   = input.string("normal", "Table text size", options=["tiny", "small", "normal", "large"], group=gF)
tblSize  = tblSzS == "tiny" ? size.tiny : tblSzS == "small" ? size.small : tblSzS == "large" ? size.large : size.normal

// ───────── Timeframe checks
chartSec = timeframe.in_seconds()
pTF      = profTF == "" ? timeframe.period : profTF
profSec  = timeframe.in_seconds(pTF)
srcSec   = timeframe.in_seconds(srcTF)

if profSec < chartSec or profSec % chartSec != 0
    runtime.error("Profile TF must be >= chart TF and a whole multiple of it (e.g. chart 1m, profile 5m)")
if srcSec > chartSec
    runtime.error("Source TF must be <= chart TF (e.g. chart 1m -> source 1m or seconds)")

inCandle = profSec == chartSec
barsPer  = profSec / chartSec
srcPer   = profSec / srcSec          // source candles per profile candle (e.g. 5)

// ───────── Source data for the current chart bar
[lo1, lh1, ll1, lc1, lv1] = request.security_lower_tf(syminfo.tickerid, srcTF, [open, high, low, close, volume])
sameTF = srcSec == chartSec
o1 = sameTF ? array.from(open)   : lo1
h1 = sameTF ? array.from(high)   : lh1
l1 = sameTF ? array.from(low)    : ll1
c1 = sameTF ? array.from(close)  : lc1
v1 = sameTF ? array.from(nz(volume)) : lv1

// ───────── Profile math
f_prof(int nrows, array<float> o, array<float> h, array<float> l, array<float> c, array<float> v) =>
    buyV  = array.new_float(nrows, 0.0)
    sellV = array.new_float(nrows, 0.0)
    float bot  = na
    float step = na
    int   n    = na(v) ? 0 : v.size()
    if n > 0
        bot  := l.min()
        step := (h.max() - bot) / nrows
        for i = 0 to n - 1
            hi  = h.get(i)
            lo  = l.get(i)
            cl  = c.get(i)
            op  = o.get(i)
            vol = nz(v.get(i))
            rng = hi - lo
            bf  = rng > 0 ? (cl - lo) / rng : cl > op ? 1.0 : cl < op ? 0.0 : 0.5
            if rng <= 0 or step <= 0
                r = step > 0 ? math.max(0, math.min(math.floor((cl - bot) / step), nrows - 1)) : 0
                buyV.set(r,  buyV.get(r)  + vol * bf)
                sellV.set(r, sellV.get(r) + vol * (1 - bf))
            else
                rs = math.max(math.floor((lo - bot) / step), 0)
                re = math.min(math.floor((hi - bot) / step), nrows - 1)
                for r = rs to re
                    rLo = bot + r * step
                    ov  = math.min(hi, rLo + step) - math.max(lo, rLo)
                    if ov > 0
                        part = vol * ov / rng
                        buyV.set(r,  buyV.get(r)  + part * bf)
                        sellV.set(r, sellV.get(r) + part * (1 - bf))
    [buyV, sellV, bot, step]

f_va(array<float> buyV, array<float> sellV, float pct) =>
    nr    = buyV.size()
    tot   = array.new_float(nr, 0.0)
    float total = 0.0
    float maxT  = 0.0
    int   pocI  = 0
    for r = 0 to nr - 1
        t = buyV.get(r) + sellV.get(r)
        tot.set(r, t)
        total += t
        if t > maxT
            maxT := t
            pocI := r
    int   vLo = pocI
    int   vHi = pocI
    float acc = maxT
    while total > 0 and acc < total * pct and (vLo > 0 or vHi < nr - 1)
        up = vHi < nr - 1 ? tot.get(vHi + 1) : -1.0
        dn = vLo > 0      ? tot.get(vLo - 1) : -1.0
        if up >= dn
            vHi += 1
            acc += up
        else
            vLo -= 1
            acc += dn
    [tot, total, maxT, pocI, vLo, vHi]

// ───────── Predictor math
// standard normal CDF (Abramowitz-Stegun erf approximation)
f_phi(float x) =>
    z  = math.abs(x) / math.sqrt(2.0)
    t  = 1.0 / (1.0 + 0.3275911 * z)
    y  = 1.0 - (((((1.061405429 * t - 1.453152027) * t) + 1.421413741) * t - 0.284496736) * t + 0.254829592) * t * math.exp(-z * z)
    x >= 0 ? 0.5 * (1.0 + y) : 0.5 * (1.0 - y)

// forces after the first m source candles of a profile candle
f_feat(array<float> o, array<float> h, array<float> l, array<float> c, array<float> v, int m, float sig) =>
    op  = o.first()
    px  = c.get(m - 1)
    tau = math.max(srcPer - m, 0) / srcPer                    // fraction of candle still to go
    z   = sig > 0 ? (px - op) / (sig * math.sqrt(math.max(tau, 0.0001))) : 0.0
    float upv = 0.0
    float tv  = 0.0
    for i = 0 to m - 1
        hi  = h.get(i)
        lo  = l.get(i)
        cl  = c.get(i)
        rng = hi - lo
        bf  = rng > 0 ? (cl - lo) / rng : cl > o.get(i) ? 1.0 : cl < o.get(i) ? 0.0 : 0.5
        vol = nz(v.get(i))
        upv += vol * bf
        tv  += vol
    dp = tv > 0 ? 2 * upv / tv - 1 : 0.0
    [u, d, bot, st]                 = f_prof(rows, o.slice(0, m), h.slice(0, m), l.slice(0, m), c.slice(0, m), v.slice(0, m))
    [t, total, maxT, poc, vlo, vhi] = f_va(u, d, vaPct)
    pb = sig > 0 and total > 0 ? (bot + (poc + 0.5) * st - op) / sig : 0.0
    [z, dp, pb]

// ───────── Drawing: horizontal profile at bar x0, widest row = wMax bars
var array<box>   lvBoxes  = array.new<box>()
var array<line>  lvLines  = array.new<line>()
var array<label> lvLabels = array.new<label>()

f_draw(int x0, int wMax, int xEnd, array<float> o, array<float> h, array<float> l, array<float> c, array<float> v, string tag, bool live) =>
    [b, s, bot, st]                 = f_prof(rows, o, h, l, c, v)
    [t, total, maxT, poc, vlo, vhi] = f_va(b, s, vaPct)
    if total > 0
        if showBox and not inCandle and xEnd > x0
            bx = box.new(x0, bot + rows * st, xEnd, bot, border_color=color.new(color.gray, 60), bgcolor=color.new(color.gray, 96))
            if live
                lvBoxes.push(bx)
        for r = 0 to rows - 1
            tr = t.get(r)
            if tr > 0
                rLo = bot + r * st
                rHi = rLo + st
                ext = r >= vlo and r <= vhi ? 0 : fadeOut
                w   = math.max(1, math.round(wMax * tr / maxT))
                wb  = math.round(w * b.get(r) / tr)
                if wb > 0
                    bb = box.new(x0, rHi, x0 + wb, rLo, border_color=chart.bg_color, bgcolor=color.new(cBuy, math.min(100, 25 + ext)))
                    if live
                        lvBoxes.push(bb)
                if w - wb > 0
                    bs = box.new(x0 + wb, rHi, x0 + w, rLo, border_color=chart.bg_color, bgcolor=color.new(cSell, math.min(100, 25 + ext)))
                    if live
                        lvBoxes.push(bs)
        pocY = bot + (poc + 0.5) * st
        lp = line.new(x0, pocY, x0 + wMax, pocY, color=cPoc, width=2)
        if live
            lvLines.push(lp)
        if showVAL
            lh = line.new(x0, bot + (vhi + 1) * st, x0 + wMax, bot + (vhi + 1) * st, color=cVA, style=line.style_dotted)
            ll = line.new(x0, bot + vlo * st,       x0 + wMax, bot + vlo * st,       color=cVA, style=line.style_dotted)
            if live
                lvLines.push(lh)
                lvLines.push(ll)
        if showDlt or tag != ""
            dlt = (b.sum() - s.sum()) / total * 100
            lb = label.new(x0, bot + rows * st, (tag != "" ? tag + "  " : "") + "Δ " + str.tostring(dlt, "#.#") + "%",
                 style=label.style_label_lower_left, color=color.new(color.gray, 75), textcolor=chart.fg_color, size=size.tiny)
            if live
                lvLabels.push(lb)

// ───────── Accumulate source candles over the current profile-TF candle
var array<float> accO = array.new_float()
var array<float> accH = array.new_float()
var array<float> accL = array.new_float()
var array<float> accC = array.new_float()
var array<float> accV = array.new_float()
var int pStart  = 0
var int pStartT = 0

var array<float> selO = na
var array<float> selH = na
var array<float> selL = na
var array<float> selC = na
var array<float> selV = na
var int selStartT = na

// predictor state
var array<float> moves = array.new_float()     // close - open of finished profile candles (for sigma)
var float nObs   = 0.0
var float hitZ   = 0.0
var float hitD   = 0.0
var float hitP   = 0.0
var float hitC   = 0.0
var float brZ    = 0.0
var float brC    = 0.0
var float agN    = 0.0
var float agHit  = 0.0
var float dsN    = 0.0
var float dsPx   = 0.0
var float hcN    = 0.0
var float hcHit  = 0.0
var float upN    = 0.0

newP = timeframe.change(pTF) or bar_index == 0

if newP
    if bar_index > 0
        // finalize the profile candle that just ended: acc + previous chart bar's source candles
        fO = accO.copy()
        fH = accH.copy()
        fL = accL.copy()
        fC = accC.copy()
        fV = accV.copy()
        fO.concat(o1[1])
        fH.concat(h1[1])
        fL.concat(l1[1])
        fC.concat(c1[1])
        fV.concat(v1[1])
        if not inCandle and pStart > last_bar_index - showN * barsPer
            f_draw(pStart, math.max(1, math.round(barsPer * widthPct / 100)), bar_index - 1, fO, fH, fL, fC, fV, "", false)
        if selOn and selT >= pStartT and selT < time
            selO := fO
            selH := fH
            selL := fL
            selC := fC
            selV := fV
            selStartT := pStartT

        // ── score the predictor on this finished candle at the checkpoint
        if predOn and fO.size() > 0
            sig = moves.size() >= 20 ? moves.stdev() : na
            isUp = fC.last() >= fO.first()
            if not na(sig) and chkM < srcPer and fO.size() > chkM
                [z, dp, pb] = f_feat(fO, fH, fL, fC, fV, chkM, sig)
                pZ = f_phi(z)
                pC = f_phi(z + wD * dp + wP * pb)
                nObs += 1
                upN  += isUp ? 1 : 0
                hitZ += (z  >= 0) == isUp ? 1 : 0
                hitD += (dp >= 0) == isUp ? 1 : 0
                hitP += (pb >= 0) == isUp ? 1 : 0
                hitC += (pC >= 0.5) == isUp ? 1 : 0
                brZ  += math.pow(pZ - (isUp ? 1 : 0), 2)
                brC  += math.pow(pC - (isUp ? 1 : 0), 2)
                if z != 0 and dp != 0
                    if math.sign(z) == math.sign(dp)
                        agN   += 1
                        agHit += (z > 0) == isUp ? 1 : 0
                    else
                        dsN  += 1
                        dsPx += (z > 0) == isUp ? 1 : 0
                if math.abs(pC - 0.5) >= 0.2
                    hcN   += 1
                    hcHit += (pC >= 0.5) == isUp ? 1 : 0
            moves.push(fC.last() - fO.first())
            if moves.size() > sigLen
                moves.shift()
    accO.clear()
    accH.clear()
    accL.clear()
    accC.clear()
    accV.clear()
    pStart  := bar_index
    pStartT := time
else
    accO.concat(o1[1])
    accH.concat(h1[1])
    accL.concat(l1[1])
    accC.concat(c1[1])
    accV.concat(v1[1])

// ───────── Chart TF == profile TF: heatmap + markers inside each candle
[bH, sH, botH, stH]              = f_prof(hmRows, o1, h1, l1, c1, v1)
[tH, totH, maxH, pocH, vlH, vhH] = f_va(bH, sH, vaPct)
[bA, sA, botA, stA]              = f_prof(rows, o1, h1, l1, c1, v1)
[tA, totA, maxA, pocA, vlA, vhA] = f_va(bA, sA, vaPct)

okA   = inCandle and totA > 0
pocPx = okA ? botA + (pocA + 0.5) * stA : na
vahPx = okA ? botA + (vhA + 1) * stA    : na
valPx = okA ? botA + vlA * stA          : na

plot(showPOC ? pocPx : na, "POC", cPoc, 4, plot.style_circles, display=display.pane)
plotchar(showVAk ? vahPx : na, "VAH", "—", location.absolute, cVA, size=size.small, display=display.pane)
plotchar(showVAk ? valPx : na, "VAL", "—", location.absolute, cVA, size=size.small, display=display.pane)

hmOn(int r) => inCandle and showHM and maxH > 0 and r < hmRows

hmLo(int r) =>
    float y = na
    if hmOn(r)
        if tH.get(r) > 0
            y := botH + r * stH
    y

hmHi(int r) => hmLo(r) + stH

hmCol(int r) =>
    color col = na
    if hmOn(r)
        t = tH.get(r)
        if t > 0
            base = color.from_gradient(bH.get(r) / t, 0.0, 1.0, cSell, cBuy)
            col := color.new(base, hmMaxT - (hmMaxT - hmMinT) * t / maxH)
    col

noWick = color.new(color.white, 100)
plotcandle(hmLo(0), hmHi(0), hmLo(0), hmHi(0), "HM row 1", hmCol(0), noWick, bordercolor=hmCol(0), display=display.pane)
plotcandle(hmLo(1), hmHi(1), hmLo(1), hmHi(1), "HM row 2", hmCol(1), noWick, bordercolor=hmCol(1), display=display.pane)
plotcandle(hmLo(2), hmHi(2), hmLo(2), hmHi(2), "HM row 3", hmCol(2), noWick, bordercolor=hmCol(2), display=display.pane)
plotcandle(hmLo(3), hmHi(3), hmLo(3), hmHi(3), "HM row 4", hmCol(3), noWick, bordercolor=hmCol(3), display=display.pane)
plotcandle(hmLo(4), hmHi(4), hmLo(4), hmHi(4), "HM row 5", hmCol(4), noWick, bordercolor=hmCol(4), display=display.pane)
plotcandle(hmLo(5), hmHi(5), hmLo(5), hmHi(5), "HM row 6", hmCol(5), noWick, bordercolor=hmCol(5), display=display.pane)
plotcandle(hmLo(6), hmHi(6), hmLo(6), hmHi(6), "HM row 7", hmCol(6), noWick, bordercolor=hmCol(6), display=display.pane)
plotcandle(hmLo(7), hmHi(7), hmLo(7), hmHi(7), "HM row 8", hmCol(7), noWick, bordercolor=hmCol(7), display=display.pane)

// ───────── Last bar: live profile candle, right-side panels, predictor table
var table tb = table.new(tblPos == "top_right" ? position.top_right : tblPos == "bottom_right" ? position.bottom_right : tblPos == "top_left" ? position.top_left : position.bottom_left,
     3, 20, bgcolor=color.new(#131722, 5), frame_color=color.new(#787B86, 40), frame_width=1, border_width=1, border_color=color.new(#2A2E39, 0))

cLbl = #D1D4DC          // labels
cVal = color.white      // neutral values
cHdr = #F7C948          // headers
cUpC = #26A69A
cDnC = #EF5350
bHdr = color.new(#2A2E39, 0)

f_cell(int col, int row, string txt, color tc, string tip = "", color bg = na) =>
    tb.cell(col, row, txt, text_color=tc, text_size=tblSize, text_halign=col == 0 ? text.align_left : text.align_right, bgcolor=bg, tooltip=tip)

f_row(int row, string lbl, string val, color vc, string extra, string tip) =>
    f_cell(0, row, lbl, cLbl, tip)
    f_cell(1, row, val, vc, tip)
    f_cell(2, row, extra, cLbl, tip)

f_hdr(int row, string a, string b, string c) =>
    f_cell(0, row, a, cHdr, "", bHdr)
    f_cell(1, row, b, cHdr, "", bHdr)
    f_cell(2, row, c, cHdr, "", bHdr)

f_pct(float a, float b) => b > 0 ? str.tostring(a / b * 100, "0.0") + "%" : "–"
f_sgn(float x) => x >= 0 ? cUpC : cDnC

if barstate.islast
    for bx in lvBoxes
        bx.delete()
    for ln in lvLines
        ln.delete()
    for lb in lvLabels
        lb.delete()
    lvBoxes.clear()
    lvLines.clear()
    lvLabels.clear()

    // current (forming) profile candle = acc + this bar's source candles
    cO = accO.copy()
    cH = accH.copy()
    cL = accL.copy()
    cC = accC.copy()
    cV = accV.copy()
    cO.concat(o1)
    cH.concat(h1)
    cL.concat(l1)
    cC.concat(c1)
    cV.concat(v1)

    xR = bar_index + lvOff
    if not inCandle
        f_draw(pStart, math.max(1, math.round(barsPer * widthPct / 100)), pStart + barsPer - 1, cO, cH, cL, cC, cV, "", true)
        xR := math.max(xR, pStart + barsPer + lvOff)
    else if showLv
        f_draw(xR, lvWidth, xR + lvWidth, o1[1], h1[1], l1[1], c1[1], v1[1], "prev", true)
        xR += lvWidth + 3
        f_draw(xR, lvWidth, xR + lvWidth, cO, cH, cL, cC, cV, "now", true)
        xR += lvWidth + 3

    if selOn
        if selT >= pStartT
            selO := cO
            selH := cH
            selL := cL
            selC := cC
            selV := cV
            selStartT := pStartT
        if not na(selO)
            f_draw(xR, lvWidth, xR + lvWidth, selO, selH, selL, selC, selV, pTF + " " + str.format_time(selStartT, "dd/MM HH:mm"), true)
        else
            lvLabels.push(label.new(xR, close, "picked candle not in loaded history", style=label.style_label_left, size=size.small))

    // ── predictor table
    if predOn
        tb.clear(0, 0, 2, 19)
        sig  = moves.size() >= 20 ? moves.stdev() : na
        mNow = cO.size()
        f_hdr(0, "LIVE  " + pTF + " candle", "value", "")
        if na(sig) or mNow == 0
            f_row(1, "warming up", "", cVal, "", "Needs 20 finished candles to estimate sigma")
        else
            [z, dp, pb] = f_feat(cO, cH, cL, cC, cV, mNow, sig)
            pC = f_phi(z + wD * dp + wP * pb)
            pZ = f_phi(z)
            f_row(1, "P(close ≥ open)", str.tostring(pC * 100, "0.0") + "%", f_sgn(pC - 0.5), pC >= 0.5 ? "▲ UP" : "▼ DOWN", "Combined probability the candle closes at or above its open")
            tb.cell_set_text_color(2, 1, f_sgn(pC - 0.5))
            f_row(2, "Elapsed", str.tostring(math.min(mNow, srcPer)) + " / " + str.tostring(srcPer), cVal, "", "Source candles completed inside this candle")
            f_row(3, "Price vs open (z)", str.tostring(z, "0.00"), f_sgn(z), str.tostring(pZ * 100, "0") + "%", "Distance from open in sigma, scaled by time left. Right column = probability from price alone")
            f_row(4, "Delta (−1…+1)", str.tostring(dp, "0.00"), f_sgn(dp), "", "Buy minus sell volume so far, as a share of total")
            f_row(5, "POC vs open (σ)", str.tostring(pb, "0.00"), f_sgn(pb), "", "Where the most-traded price sits relative to the open, in sigma")
            f_row(6, "Sigma (candle move)", str.tostring(sig, format.mintick), cVal, "", "Std-dev of open-to-close over the last finished candles")

        f_hdr(8, "HISTORY @ " + str.tostring(chkM) + "/" + str.tostring(srcPer) + "  n=" + str.tostring(nObs, "#"), "hit", "Brier")
        f_row(9,  "Base rate (always up)",  f_pct(upN, nObs),  cVal, "", "How often candles closed at or above the open")
        f_row(10, "Price vs open only",     f_pct(hitZ, nObs), cVal, nObs > 0 ? str.tostring(brZ / nObs, "0.000") : "–", "Direction of price vs open at the checkpoint")
        f_row(11, "Delta only",             f_pct(hitD, nObs), cVal, "", "Sign of delta at the checkpoint")
        f_row(12, "POC vs open only",       f_pct(hitP, nObs), cVal, "", "POC above or below the open at the checkpoint")
        f_row(13, "Combined model",         f_pct(hitC, nObs), cVal, nObs > 0 ? str.tostring(brC / nObs, "0.000") : "–", "Phi(z + wD·delta + wP·POC). Lower Brier than price-only = flow adds information")
        f_row(14, "Price & delta agree",    f_pct(agHit, agN), cVal, "n " + str.tostring(agN, "#"), "Hit rate when price and delta point the same way")
        f_row(15, "Disagree → price wins",  f_pct(dsPx, dsN),  cVal, "n " + str.tostring(dsN, "#"), "When they disagree, how often price direction was right. Under ~60% = delta matters")
        f_row(16, "High conf (P≤30/≥70%)",  f_pct(hcHit, hcN), cVal, "n " + str.tostring(hcN, "#"), "Hit rate of strong combined readings")

// alerts (chart TF = profile TF)
alertcondition(barstate.isconfirmed and okA and (pocPx - low) / math.max(high - low, syminfo.mintick) <= 0.25, "POC at candle low",  "POC in bottom 25% of candle")
alertcondition(barstate.isconfirmed and okA and (pocPx - low) / math.max(high - low, syminfo.mintick) >= 0.75, "POC at candle high", "POC in top 25% of candle")

